/*
 * =============================================================================
 *
 *   Copyright (c) 2014-2017, The UNBESCAPE team (http://www.unbescape.org)
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 * =============================================================================
 */

package org.pentaho.gwt.widgets.client.utils.string;

import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.Widget;

import java.util.Arrays;

// The code of this class was adapted from UNBESCAPE. Because this code will all be transpiled to JavaScript
// by GWT, special care is needed on the included functionality. Only the strictly needed functionality
// from the comprehensive library was included.
// See https://github.com/unbescape/unbescape/blob/master/src/main/java/org/unbescape/css/CssIdentifierEscapeUtil.java.
public class CssUtils {
  /*
   * Prefixes defined for use in escape and unescape operations
   */
  private static final char ESCAPE_PREFIX = '\\';

  /*
   * Small utility char arrays for hexadecimal conversion.
   */
  private static char[] HEXA_CHARS_UPPER = "0123456789ABCDEF".toCharArray();


  /*
   * Structures for holding the Backslash Escapes
   */
  private static int BACKSLASH_CHARS_LEN = '~' + 1; // 0x7E + 1 = 0x7F
  private static char BACKSLASH_CHARS_NO_ESCAPE = 0x0;
  private static char[] BACKSLASH_CHARS;

  /*
   * Structured for holding the 'escape level' assigned to chars (not codepoints) up to ESCAPE_LEVELS_LEN.
   * - The last position of the ESCAPE_LEVELS array will be used for determining the level of all
   *   codepoints >= (ESCAPE_LEVELS_LEN - 1)
   */
  private static final char ESCAPE_LEVELS_LEN = 0x9f + 2; // Last relevant char to be indexed is 0x9f
  private static final byte[] ESCAPE_LEVELS;

  static {
    /*
     * Initialize Single Escape Characters
     */
    BACKSLASH_CHARS = new char[BACKSLASH_CHARS_LEN];
    Arrays.fill(BACKSLASH_CHARS, BACKSLASH_CHARS_NO_ESCAPE);
    BACKSLASH_CHARS[0x20] = ' ';
    BACKSLASH_CHARS[0x21] = '!';
    BACKSLASH_CHARS[0x22] = '"';
    BACKSLASH_CHARS[0x23] = '#';
    BACKSLASH_CHARS[0x24] = '$';
    BACKSLASH_CHARS[0x25] = '%';
    BACKSLASH_CHARS[0x26] = '&';
    BACKSLASH_CHARS[0x27] = '\'';
    BACKSLASH_CHARS[0x28] = '(';
    BACKSLASH_CHARS[0x29] = ')';
    BACKSLASH_CHARS[0x2A] = '*';
    BACKSLASH_CHARS[0x2B] = '+';
    BACKSLASH_CHARS[0x2C] = ',';
    // hyphen: will only be escaped when identifer starts with '--' or '-{digit}'
    BACKSLASH_CHARS[0x2D] = '-';
    BACKSLASH_CHARS[0x2E] = '.';
    BACKSLASH_CHARS[0x2F] = '/';
    // colon: will not be used for escaping: not recognized by IE < 8
    // BACKSLASH_CHARS[0x3A] = ':';
    BACKSLASH_CHARS[0x3B] = ';';
    BACKSLASH_CHARS[0x3C] = '<';
    BACKSLASH_CHARS[0x3D] = '=';
    BACKSLASH_CHARS[0x3E] = '>';
    BACKSLASH_CHARS[0x3F] = '?';
    BACKSLASH_CHARS[0x40] = '@';
    BACKSLASH_CHARS[0x5B] = '[';
    BACKSLASH_CHARS[0x5C] = '\\';
    BACKSLASH_CHARS[0x5D] = ']';
    BACKSLASH_CHARS[0x5E] = '^';
    // underscore: will only be escaped at the beginning of an identifier (in order to avoid issues in IE6)
    BACKSLASH_CHARS[0x5F] = '_';
    BACKSLASH_CHARS[0x60] = '`';
    BACKSLASH_CHARS[0x7B] = '{';
    BACKSLASH_CHARS[0x7C] = '|';
    BACKSLASH_CHARS[0x7D] = '}';
    BACKSLASH_CHARS[0x7E] = '~';

    /*
     * Initialization of escape levels.
     * Defined levels :
     *
     *    - Level 1 : Basic escape set
     *    - Level 2 : Basic escape set plus all non-ASCII
     *    - Level 3 : All non-alphanumeric characters
     *    - Level 4 : All characters
     *
     */
    ESCAPE_LEVELS = new byte[ESCAPE_LEVELS_LEN];

    /*
     * Everything is level 3 unless contrary indication.
     */
    Arrays.fill(ESCAPE_LEVELS, (byte)3);

    /*
     * Everything non-ASCII is level 2 unless contrary indication.
     */
    for (char c = 0x80; c < ESCAPE_LEVELS_LEN; c++) {
      ESCAPE_LEVELS[c] = 2;
    }

    /*
     * Alphanumeric characters are level 4.
     */
    for (char c = 'A'; c <= 'Z'; c++) {
      ESCAPE_LEVELS[c] = 4;
    }
    for (char c = 'a'; c <= 'z'; c++) {
      ESCAPE_LEVELS[c] = 4;
    }
    for (char c = '0'; c <= '9'; c++) {
      ESCAPE_LEVELS[c] = 4;
    }

    /*
     * Backslash Escapes will be level 1 (always escaped)
     */
    ESCAPE_LEVELS[0x20] = 1;
    ESCAPE_LEVELS[0x21] = 1;
    ESCAPE_LEVELS[0x22] = 1;
    ESCAPE_LEVELS[0x23] = 1;
    ESCAPE_LEVELS[0x24] = 1;
    ESCAPE_LEVELS[0x25] = 1;
    ESCAPE_LEVELS[0x26] = 1;
    ESCAPE_LEVELS[0x27] = 1;
    ESCAPE_LEVELS[0x28] = 1;
    ESCAPE_LEVELS[0x29] = 1;
    ESCAPE_LEVELS[0x2A] = 1;
    ESCAPE_LEVELS[0x2B] = 1;
    ESCAPE_LEVELS[0x2C] = 1;
    // hyphen: will only be escaped when identifer starts with '--' or '-{digit}'
    ESCAPE_LEVELS[0x2D] = 1;
    ESCAPE_LEVELS[0x2E] = 1;
    ESCAPE_LEVELS[0x2F] = 1;
    // colon: will not be used for escaping: not recognized by IE < 8
    ESCAPE_LEVELS[0x3A] = 1;
    ESCAPE_LEVELS[0x3B] = 1;
    ESCAPE_LEVELS[0x3C] = 1;
    ESCAPE_LEVELS[0x3D] = 1;
    ESCAPE_LEVELS[0x3E] = 1;
    ESCAPE_LEVELS[0x3F] = 1;
    ESCAPE_LEVELS[0x40] = 1;
    ESCAPE_LEVELS[0x5B] = 1;
    ESCAPE_LEVELS[0x5C] = 1;
    ESCAPE_LEVELS[0x5D] = 1;
    ESCAPE_LEVELS[0x5E] = 1;
    // underscore: will only be escaped at the beginning of an identifier (in order to avoid issues in IE6)
    ESCAPE_LEVELS[0x5F] = 1;
    ESCAPE_LEVELS[0x60] = 1;
    ESCAPE_LEVELS[0x7B] = 1;
    ESCAPE_LEVELS[0x7C] = 1;
    ESCAPE_LEVELS[0x7D] = 1;
    ESCAPE_LEVELS[0x7E] = 1;

    /*
     * Two ranges of non-displayable, control characters:
     * U+0000 to U+001F and U+007F to U+009F.
     */
    for (char c = 0x00; c <= 0x1F; c++) {
      ESCAPE_LEVELS[c] = 1;
    }
    for (char c = 0x7F; c <= 0x9F; c++) {
      ESCAPE_LEVELS[c] = 1;
    }
  }

  private static char[] toCompactHexa( final int codepoint, final char next, final int level ) {

    // In identifiers, whitespace will always be escaped (no need for trailing whitespace)
    // If level is 4, hexadecimal characters will be escaped (no need to for trailing whitespaces)
    final boolean needTrailingSpace =
        (level < 4 && ((next >= '0' && next <= '9') || (next >= 'A' && next <= 'F') || (next >= 'a' && next <= 'f')));

    if (codepoint == 0) {
      return (needTrailingSpace ? new char[] { '0', ' ' } : new char[] { '0' });
    }
    int div = 20;
    char[] result = null;
    while (result == null && div >= 0) {
      if ((codepoint >>> div) % 0x10 > 0) {
        result = new char[(div / 4) + (needTrailingSpace? 2 : 1)];
      }
      div -= 4;
    }
    div = 0;
    for (int i = (needTrailingSpace? result.length - 2 : result.length - 1); i >= 0; i--) {
      result[i] = HEXA_CHARS_UPPER[(codepoint >>> div) % 0x10];
      div += 4;
    }
    if (needTrailingSpace) {
      result[result.length - 1] = ' ';
    }

    return result;
  }

  public static String escape( final String text  ) {
    final int level = 3;
    return escape( text, level );
  }

  /*
   * Perform an escape operation, based on String, according to the specified level and type.
   */
  private static String escape( final String text, final int level ) {

    if (text == null) {
      return null;
    }

    StringBuilder strBuilder = null;

    final int offset = 0;
    final int max = text.length();

    int readOffset = offset;

    for (int i = offset; i < max; i++) {

      final int codepoint = Character.codePointAt(text, i);

      /*
       * Shortcut: most characters will be ASCII/Alphanumeric, and we won't need to do anything at
       * all for them
       */
      if (codepoint <= (ESCAPE_LEVELS_LEN - 2) && level < ESCAPE_LEVELS[codepoint] &&
          (i > offset || codepoint < '0' || codepoint > '9')) {
        // Note how we check whether the first char is a decimal number, in which case we have to escape it
        continue;
      }

      /*
       * Hyphen check: only escape when it's the first char and it's followed by '-' or a digit.
       */
      if (codepoint == '-' && level < 3) {
        if (i > offset || i + 1 >= max) {
          continue;
        }
        final char c1 = text.charAt(i + 1);
        if (c1 != '-' && (c1 < '0' || c1 > '9')) {
          continue;
        }
      }

      /*
       * Underscore check: only escape when it's the first char.
       */
      if (codepoint == '_' && level < 3 && i > offset) {
        continue;
      }

      /*
       * Shortcut: we might not want to escape non-ASCII chars at all either.
       */
      if (codepoint > (ESCAPE_LEVELS_LEN - 2) && level < ESCAPE_LEVELS[ESCAPE_LEVELS_LEN - 1]) {

        if (Character.charCount(codepoint) > 1) {
          // This is to compensate that we are actually escaping two char[] positions with a single codepoint.
          i++;
        }

        continue;
      }

      /*
       * At this point we know for sure we will need some kind of escape, so we
       * can increase the offset and initialize the string builder if needed, along with
       * copying to it all the contents pending up to this point.
       */

      if (strBuilder == null) {
        strBuilder = new StringBuilder(max + 20);
      }

      if (i - readOffset > 0) {
        strBuilder.append(text, readOffset, i);
      }

      if (Character.charCount(codepoint) > 1) {
        // This is to compensate that we are actually reading two char[] positions with a single codepoint.
        i++;
      }

      readOffset = i + 1;

      /*
       * ------------------------------------------------------------------------------------------
       *
       * Perform the real escape, attending the different combinations of BACKSLASH and HEXA escapes
       *
       * ------------------------------------------------------------------------------------------
       */

      if (codepoint < BACKSLASH_CHARS_LEN) {
        // We will try to use a BACKSLASH ESCAPE

        final char escape = BACKSLASH_CHARS[codepoint];

        if (escape != BACKSLASH_CHARS_NO_ESCAPE) {
          // Escape found! just write it and go for the next char
          strBuilder.append(ESCAPE_PREFIX);
          strBuilder.append(escape);
          continue;
        }
      }

      /*
       * No escape was possible, so we need hexa escape (compact or 6-digit).
       */

      final char next =
          ((i + 1 < max) ? text.charAt(i + 1) : (char) 0x0);

      strBuilder.append(ESCAPE_PREFIX);
      strBuilder.append(toCompactHexa(codepoint, next, level));
    }


    /*
     * -----------------------------------------------------------------------------------------------
     * Final cleaning: return the original String object if no escape was actually needed. Otherwise
     *                 append the remaining unescaped text to the string builder and return.
     * -----------------------------------------------------------------------------------------------
     */

    if (strBuilder == null) {
      return text;
    }

    if (max - readOffset > 0) {
      strBuilder.append(text, readOffset, max);
    }

    return strBuilder.toString();

  }

  /**
   * Used to bypass width unit restrictions without re-wrapping GWT objects
   * e.g. to use `calc(...)` function or `vw`
   *
   * This concept could be generalized further to address, say, height restrictions.
   * This works for now.
   */
  public static void setElementWidth( Widget widget, String widthString){
    Element element = widget.getElement();
    // clear the property if it exists
    String style = widget.getElement().getAttribute( "style" );
    widget.getElement().getStyle().clearWidth();
    String styleString = element.getAttribute( "style" );
    String newStyleString = styleString + " width: " + widthString + ";";
    element.setAttribute( "style", newStyleString );
  }
}
